Skip to main content

Browser-apis

Data Attributes and the dataset API

Definition

  • Custom data attributes start with data- and are accessible through the dataset property.

Reading Data Attributes

<div
id="user"
data-user-id="123"
data-role="admin"
data-is-active="true"
>
John Doe
</div>


const user = document.querySelector('#user')

// Read data attributes (camelCase!)
console.log(user.dataset.userId) // "123"
console.log(user.dataset.role) // "admin"

Adding or Updating Data Attributes

user.dataset.lastLogin = '2024-01-15'

// Creates:
// data-last-login="2024-01-15"

Deleting Data Attributes

delete user.dataset.role

Event Delegation in JavaScript

Definition

  • Event delegation is a technique where, instead of attaching event listeners to multiple child elements, you attach a single event listener to a parent element and use event.target to determine which child triggered the event.
  • This approach:
    • Uses less memory.
    • Scales better when working with many elements.
    • Works for elements added later without attaching new event listeners.

Example 1

  • Adding event listeners to every item does not scale.
// HTML:
// <ul id="menu">
// <li class="todo-item"><button>Click</button></li>
// </ul>

document.querySelectorAll('.li').forEach(item => {
item.addEventListener('click', handleClick)
})

// Items added later won't have event listeners.

The Solution

Attach a single event listener to the parent element.

document.querySelector('.menu').addEventListener('click', (event) => {
// event.target.tagName -> The element that was clicked.
// event.currentTarget.tagName -> The element where the listener is attached.

// matches() only works if the clicked element itself matches '.todo-item'.
// It won't match if, for example, a button inside the <li> is clicked.
if (event.target.matches('.todo-item')) {
handleClick(event)
}
})

Example 2: Element.closest()

Definition: closest()

  • The closest() method traverses up the DOM tree to find the nearest ancestor (or the element itself) that matches a selector.
// HTML:
//
// <ul id="todo-list">
// <li data-id="1">Buy groceries</li>
// <li data-id="2">Walk the dog</li>
// <li data-id="3">Finish report</li>
// </ul>

const todoList = document.getElementById('todo-list')

todoList.addEventListener('click', (event) => {
// Works regardless of where inside the <li> the user clicks.
const item = event.target.closest('li')

if (item) {
const id = item.dataset.id

console.log(`Clicked todo item with id: ${id}`)

item.classList.toggle('completed')
}
})

Custom Events

What Are Custom Events?

  • Custom events let you create your own event types, attach any data you want, and build applications where components communicate through events instead of direct function calls.
  • Use cases:
    1. Notify the application when a modal opens or closes. Listeners then can
      • Pause background videos.
      • Disable page scrolling.
    2. Shopping Cart Updates, Instead of updating multiple UI elements manually. Listeners then can
      • Update the cart badge.
      • Recalculate totals.
    3. A notification component listens and displays the message.
    4. Components can update app theme themselves without knowing who initiated the change.

Example 1

const event = new CustomEvent('userLoggedIn', {
detail: {
username: 'alice',
timestamp: Date.now()
}
})

// Listen for the event anywhere in your app
document.addEventListener('userLoggedIn', (e) => {
console.log(`Welcome, ${e.detail.username}!`)
})

// Dispatch the event
document.dispatchEvent(event)

// Output:
// Welcome, alice!

Observer APIs

Intersection Observer

Definition

  • The Intersection Observer API lets you detect when an element enters, exits, or crosses a specified visibility threshold within a viewport or container element.
  • Performance
    • Runs off the main thread.
    • Optimized by the browser, making it much more efficient.
  • Use Cases
    • Lazy-load images only when they intersect with the viewport.
    • Build infinite scrolling by loading new items when the bottom is/about reached.
    • Trigger scroll-based animations when elements enter the viewport.

Example

// Lazy-load images when they come into view
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src

// Never forget to disconnect observers, which can lead to memory leaks.
observer.unobserve(img)
}
})
})

document
.querySelectorAll('img[data-src]')
.forEach(img => observer.observe(img))

MutationObserver

Definition

  • MutationObserver watches for DOM changes. It fires a callback when elements are added or removed, attributes change, or text content changes.
  • MutationObserver is asynchronous and batches changes.
  • Use Cases
    • Automatically highlight code blocks added to the page.
    • Block ads or unwanted elements injected by third-party scripts.
    • Detect when form content changes and trigger auto-save.

Example 1

// Watch for any changes to a DOM element
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
console.log('Children changed!')
console.log('Added:', mutation.addedNodes)
console.log('Removed:', mutation.removedNodes)
}

if (mutation.type === 'attributes') {
console.log(`Attribute "${mutation.attributeName}" changed`)
}
}
})

const targetElement = document.getElementById('app')

observer.observe(targetElement, {
childList: true, // Watch for added/removed children
attributes: true, // Watch for attribute changes
characterData: true // Watch for text content changes
})

Example 2: Blocking Injected Elements

const adBlocker = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== Node.ELEMENT_NODE) continue

if (node.matches('.ad-banner, [data-ad], .sponsored')) {
node.remove()
console.log('Blocked unwanted element')
}
}
}
})

adBlocker.observe(document.body, {
childList: true,
subtree: true
})

ResizeObserver

Definition

  • The ResizeObserver API lets you watch elements for size changes and react accordingly.
  • Use Cases
    • Adjust font size based on container width without media queries.
    • Keep a canvas sharp at any size by matching its internal resolution.
    • Keep a chat window scrolled to the bottom when new messages arrive.
    • Maintain the aspect ratio of responsive video or image containers.

Example

const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('Element resized:', entry.target)
console.log('New width:', entry.contentRect.width)
console.log('New height:', entry.contentRect.height)
}
})

observer.observe(document.querySelector('.resizable-box'))

PerformanceObserver

Definition

  • The Performance Observer API lets you monitor performance metrics in real time. Instead of polling for data, you subscribe to specific performance events and get notified when they occur.
  • Used By
    • Tools like Google Analytics to measure Core Web Vitals.

Some Entry Types

Entry TypeDescriptionUse
navigationPage navigation timingMeasure page load performance
resourceNetwork requests for scripts, styles, images, etc.Track asset loading times
paintFirst Paint (FP) and First Contentful Paint (FCP)Track rendering milestones
largest-contentful-paintLCP metric (Core Web Vital)Measure loading performance

The buffered: true Option

  • The buffered option captures performance entries that occurred before the observer started listening.

Example

// Create an observer with a callback function
const observer = new PerformanceObserver((list, observer) => {
// Called whenever new performance entries are recorded
const entries = list.getEntries()

entries.forEach((entry) => {
console.log(`Entry type: ${entry.entryType}`)
console.log(`Name: ${entry.name}`)
console.log(`Start time: ${entry.startTime}`)
console.log(`Duration: ${entry.duration}`)
})
})

// Start observing specific entry types
observer.observe({
entryTypes: ['resource', 'navigation'],
buffered: true
})

Blob and File API

Blob

Definition

  • A Blob (Binary Large Object) is an immutable, file-like object that represents raw binary data.

Example

// Creating Blobs from different data types
const textBlob = new Blob(['Hello, World!'], {
type: 'text/plain'
})

const jsonBlob = new Blob(
[JSON.stringify({ name: 'Alice' })],
{ type: 'application/json' }
)

const htmlBlob = new Blob(['<h1>Title</h1>'], {
type: 'text/html'
})

console.log(textBlob.size) // 13 (bytes)
console.log(textBlob.type) // "text/plain"

  • You can create a url from a blob
    • You need to clean the object with revokeObjectURL
const url = URL.createObjectURL(jsonBlob)

const link = document.createElement('a')
link.href = url
link.download = 'hello.txt'
link.click()

URL.revokeObjectURL(url) // Clean up memory

File API

Getting Files from User Input

// HTML:
// <input type="file" id="fileInput" multiple>

const fileInput = document.getElementById('fileInput')

fileInput.addEventListener('change', (event) => {
const files = event.target.files // FileList object

for (const file of files) {
console.log('Name:', file.name) // "photo.jpg"
console.log('Size:', file.size) // 1024000 (bytes)
console.log('Type:', file.type) // "image/jpeg"
console.log('Modified:', file.lastModified) // 1704067200000 (timestamp)
console.log('Modified Date:', new Date(file.lastModified))
}
})

Creating File Objects Programmatically

  • A File is a Blob that represents an actual file.
    • A File inherits from Blob and adds file-specific metadata.
    • Addition metadata File adds include
      • file.name
      • file.lastModified
// Syntax:
// new File(fileBits, fileName, options)

const file = new File(
['Hello, World!'], // Content (same as Blob)
'greeting.txt', // File name
{
type: 'text/plain', // MIME type
lastModified: Date.now() // Optional timestamp
}
)

console.log(file.name) // "greeting.txt"
console.log(file.size) // 13
console.log(file.type) // "text/plain"

Uploading A File in Chunks

async function uploadInChunks(file, chunkSize = 1024 * 1024) { // 1 MB chunks
const totalChunks = Math.ceil(file.size / chunkSize)

for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize
const end = Math.min(start + chunkSize, file.size)
const chunk = file.slice(start, end)

const formData = new FormData()
formData.append('chunk', chunk)
formData.append('chunkIndex', i)
formData.append('totalChunks', totalChunks)
formData.append('filename', file.name)

await fetch('/api/upload-chunk', {
method: 'POST',
body: formData
})

console.log(`Uploaded chunk ${i + 1}/${totalChunks}`)
}
}

Web Worker

  • Mozilla Web Workers documentation
  • Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface.
    • In simple terms, web Workers give you parallelism, while async/await and Promises give you concurrency
  • What you can and can't inside web workers
    • You can run whatever code you like inside the worker thread, with some exceptions.
      • For example, you can't directly manipulate the DOM from inside a worker, or use some default methods and properties of the window object
    • You can use a large number of items available under window, including WebSockets, and data storage mechanisms like IndexedDB.
  • There are different types of Web workers with different purposes
    • Dedicated Workers
      • Background computation, within a single tab
    • Shared Workers
      • Shared computation across tabs
    • Service Workers
      • Network proxy, offline

Create a Web Worker

const worker = new Worker('worker-file.js');

Send Message & Responses

  • Both the client and server use postMessage to send messages & responses to each other
    • The client uses it to send request
    • The server uses it to send response
const worker = new Worker('worker.js', { type: 'module' })

worker.postMessage({ task: 'process', data: [1, 2, 3] })

worker.onmessage = (event) => {
console.log('Result:', event.data)
}

worker.onerror = (event) => {
console.error('Worker error:', event.message)
}

Receive Message & Response

  • Both client and server respond to messages via the onmessage event handler
    • The message is contained within the message event's data attribute.
    • The data is copied rather than shared, which avoid traditional race conditions issues.
      • If you have a large data like image which you don't want to be copied, you can use a more efficient Transfer Ownership option to transfer the objects ownership instead of copy transfer.
// worker.js

// loads script (old way), processData comes from global scope
importScripts('./utils.js')

// Standard ES modules! (modern way)
import { processData } from './utils.js'

self.onmessage = (event) => {
const { task, data } = event.data

if (task === 'process') {
const result = processData(data)
self.postMessage(result)
}
}

Scheduling APIs

requestAnimationFrame

Definition

  • requestAnimationFrame (often abbreviated as rAF) is a browser API that tells the browser you want to perform an animation.
  • It requests a callback to be executed just before the browser performs its next repaint, typically at 60 frames per second (60 FPS) on most displays.
  • Note
    • requestAnimationFrame is one-shot by design. You must call requestAnimationFrame() inside the callback to continue the animation.

Example

// The browser calls this function when it's ready to paint
function drawFrame(timestamp) {
// timestamp = milliseconds since page load
console.log(`Frame at ${timestamp}ms`)

// Do your animation work here
updatePosition()

// Request the next frame
requestAnimationFrame(drawFrame)
}

// Start the animation loop
requestAnimationFrame(drawFrame)

Example

const box = document.getElementById('box');

let position = 0;

function animate() {
position += 2; // move 2px per frame

box.style.left = position + 'px';

// stop condition
if (position < 500) {
requestAnimationFrame(animate);
}
}

// start animation
requestAnimationFrame(animate);

requestIdleCallback

  • requestIdleCallback() lets the browser run work when it is idle and not busy handling:
    • User input
    • Rendering
    • Animations
    • Higher-priority tasks

Example:

requestIdleCallback(() => {
expensiveAnalyticsWork();
});

Useful for:

  • Analytics
  • Logging
  • Prefetching data
  • Non-critical computations

Example with remaining idle time:

requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0) {
processNextItem();
}
});

Example with timeout:

requestIdleCallback(
() => {
saveAnalytics();
},
{ timeout: 2000 }
);

This guarantees the callback runs within ~2 seconds even if the browser never becomes idle.

Caveats

  • Not supported in all browsers.
  • Never use for critical UI updates.
  • Browser decides when "idle" time exists.

For animation work, prefer:

requestAnimationFrame()

For background/non-urgent work, prefer:

requestIdleCallback()